# -*- encoding:utf-8 -*-
"""
-文件：blockchain.py
-说明：区块链类
-作者：tlj
"""
import collections
from datetime import datetime
from .db import DB
from .proof_of_work import ProofOfWork
from .utxo_set import UTXOSet
from transaction.transaction import *


class BlockChainAlreadyExists(Exception):
    pass


class NeedInitAccount(Exception):
    pass


class TransactionNotFound(Exception):
    pass


class BlockChain:
    LAST_BLOCK_HASH_KEY = 'LAST'
    # 数据库中最后一条数据的key，对应的value是最后一个区块头的hash
    DB_FILE = "data/bc_blocks_{}.db"
    # 区块链数据保持为止

    def __init__(self, node_id,init_account=None):
        self.node_id = node_id
        self.work = ProofOfWork()
        self._db = DB(BlockChain.DB_FILE.format(node_id))

        try:
            self._tail = self._db.get(BlockChain.LAST_BLOCK_HASH_KEY)
            if init_account is not None:
                raise BlockChainAlreadyExists()
            print('Open a blockchain')
        except KeyError:
            if init_account is None:
                raise NeedInitAccount()
            coinbase_tx = CoinbaseTx(init_account)

            genesis_block = self.work.mine_block([coinbase_tx], '', 1)

            self._put_block(genesis_block)
            print('created a new blockchain')

    def _put_block(self, block):
        """增加新的块到区块链中"""
        # self._db.delete(BlockChain.LAST_BLOCK_HASH_KEY)
        self._db.put(block.hash_(), block)
        self._db.put(BlockChain.LAST_BLOCK_HASH_KEY, block.hash_())
        self._db.commit()
        self._tail = block.hash_()

    def mine_block(self, data):

        try:
            last_block = self._db.get(self._tail)
            height = last_block.height+1
        except KeyError:
            height = 1
        new_block = self.work.mine_block(data, self._tail, height)
        for tx in new_block.data:
            if not self.verify_transaction(tx):
                print('Error: Invalid transaction')
                return
        self._put_block(new_block)
        return new_block

    def get_best_height(self):
        try:
            last_block = self._db.get(self._tail)
            height = last_block.height + 1
        except KeyError:
            height = 1
        return height

    def add_block(self, block):
        if not self.work.validate_block(block):
            print('无效的区块')
            return
        try:
            block_in_db = self._db.get(block.hash_())
        except KeyError:
            self._db.put(block.hash_(), block)
            last_block = self._db.get(self._tail)
            if block.height>last_block.height:
                self._db.put(BlockChain.LAST_BLOCK_HASH_KEY, block.hash_())
                self._tail = block.hash_()
            self._db.commit()

    def get_blockhashes(self):
        blocks_hash = list(self._db.kv.keys())
        blocks_hash.remove(BlockChain.LAST_BLOCK_HASH_KEY)
        return blocks_hash

    def get_block(self, block_hash):
        try:
            block = self._db.get(block_hash)
            return block
        except KeyError:
            return None

    def verify_transaction(self, tx):
        if isinstance(tx, CoinbaseTx):
            return True
        utxo_set = UTXOSet(self)
        prev_txs = {}
        for vin in tx.vins:
            # prev_tx = self.find_transaction(vin.tx_id)
            prev_tx = utxo_set.find_transaction_utxo(vin.tx_id)
            print(prev_tx)
            prev_txs[vin.tx_id] = prev_tx[0][0]
        return tx.verify(prev_txs)

    def find_transaction(self, tx_id):
        """
        :param tx_id:
        :return: 对应交易id的交易
        """
        for block in self.blocks:
            for tx in block.data:
                if tx.id_ == tx_id:
                    return tx
        raise TransactionNotFound

    def sign_transaction(self, tx, sign_key):
        """
        对交易签名
        :param tx:
        :param sign_key:
        :return:
        """
        if isinstance(tx, CoinbaseTx):
            return
        tx.sign(sign_key)

    @property
    def blocks(self):
        current = self._tail
        while True:
            if not current:
                return
            block = self._db.get(current)
            yield block
            current = block.prev_block_hash

    def print_chain(self):
        for block in self.blocks:
            print(block)
            print('Block Hash: {}'.format(block.hash_()))
            print('Prev Block Hash: {}'.format(block.prev_block_hash))
            print('TimeStamp: {}'.format(datetime.fromtimestamp(block.timestamp)))
            print('Nonce: {}'.format(block.nonce))
            print('Data: {}'.format(block.data))
            print('POW: {}'.format(self.work.validate_block(block)))
            print('Height: {}'.format(block.height))
            print()

    def find_sependable_outputs(self, address, amount):
        accumulated = 0
        unspent_outputs = collections.defaultdict(list)
        # 拥有改地址的可花费输出的交易
        unspent_txs = self.find_unspent_transactions(address)
        for tx in unspent_txs:
            for idx, out in enumerate(tx.vouts):
                # 记录属于该地址的输出
                if out.is_locked_with(address) and accumulated<amount:
                    accumulated +=out.amount
                    unspent_outputs[tx.id_].append(idx)
        return accumulated, unspent_outputs

    def find_unspent_transactions(self, address):
        unspent_txs = []
        spent_outputs = collections.defaultdict(list)
        for block in self.blocks:
            for tx in block.data:
                if not isinstance(tx, CoinbaseTx):
                    for vin in tx.vins:
                        if vin.ref_address(address):
                            # 所有属于该账户的输入中，记录所引用的输出的下标，
                            spent_outputs[vin.tx_id].append(vin.vout_idx)

                # 遍历属于该用户的输出
                for idx, out in enumerate(tx.vouts):
                    if not out.is_locked_with(address):
                        continue
                    _spent_flag = False

                    if len(spent_outputs[tx.id_]) > 0:
                        for spent_idx in spent_outputs[tx.id_]:
                            if spent_idx == idx:
                                # 如果某个交易输出已经被引用了，
                                _spent_flag = True
                                break
                    if not _spent_flag:
                        unspent_txs.append(tx)

        return unspent_txs

    def find_utxo(self, address):
        """
        :param address:
        :return: 所有可以被 address 花费的交易输出组成的列表
        """
        utxos = []
        unspent_txs = self.find_unspent_transactions(address)

        for tx in unspent_txs:
            for out in tx.vouts:
                if out.is_locked_with(address):
                    utxos.append(out)

        return utxos

    def find_utxo1(self):
        """
        寻找所有可被引用的输出
        :return:
        """
        utxos = collections.defaultdict(list)
        spent_outputs = collections.defaultdict(list)

        for block in self.blocks:
            for tx in block.data:
                if not isinstance(tx, CoinbaseTx):
                    for vin in tx.vins:
                        # 记录已引用的交易的输出的下标
                        spent_outputs[vin.tx_id].append(vin.vout_idx)
                # 遍历所有交易的输出，如果没有被引用则可花费
                for idx, out in enumerate(tx.vouts):
                    _spent_flag = False
                    if len(spent_outputs[tx.id_]) !=0:
                        for spent_idx in spent_outputs[tx.id_]:
                            if spent_idx == idx:
                                _spent_flag = True
                                break
                    if not _spent_flag:
                        utxos[tx.id_].append([out, idx])
                        # utxos[tx.id_].append(tx)

        return utxos
    



